package de.stefanocke.roo.wicket.registry;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.springframework.roo.classpath.TypeLocationService;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails;
import org.springframework.roo.classpath.details.ItdTypeDetails;
import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem;
import org.springframework.roo.metadata.MetadataIdentificationUtils;
import org.springframework.roo.metadata.MetadataItem;
import org.springframework.roo.metadata.MetadataService;
import org.springframework.roo.model.JavaType;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;

import de.stefanocke.roo.wicket.MidUtils;
import de.stefanocke.roo.wicket.base.ItdMetadata;
import de.stefanocke.roo.wicket.base.ModelClassAware;

@Component
@Service
public class MetadataRegistryImpl implements MetadataRegistry {
	@Reference
	MetadataService metadataService;

	@Reference
	TypeLocationService typeLocationService;

	private SetMultimap<JavaType, String> modelClassToMids = HashMultimap.create();
	private Map<String, JavaType> midToModelClass = new HashMap<String, JavaType>();

	private SetMultimap<String, String> kindToClasses = HashMultimap.create();
	private Map<String, String> classToKind = new HashMap<String, String>();

	private SetMultimap<WicketMetadataKey, String> modelClassAndClassOrKindToClientMids = HashMultimap.create();

	private SetMultimap<String, String> metaDataClassToMids = HashMultimap.create();

	private Set<String> scannedMetadataClasses = new HashSet<String>();

	private SetMultimap<String, Class<?>> metadataClassToTriggers = HashMultimap.create();

	@Override
	public Set<String> findMetadata(String clientMid, Class<?> classOrKind, JavaType modelClass) {
		if (classOrKind == null && modelClass == null) {
			throw new IllegalArgumentException("At least one of classOrKind or modelClass must be set.");
		}
		String classOrKindName = (classOrKind == null) ? null : classOrKind.getName();
		// register the dependency
		modelClassAndClassOrKindToClientMids.put(key(modelClass, classOrKindName), clientMid);

		Set<String> mids;
		if (classOrKindName != null) {
			mids = findMetadataForClassOrKind(clientMid, classOrKindName);
			if (modelClass != null) {
				mids = new HashSet<String>(mids);
				mids.retainAll(modelClassToMids.get(modelClass));
			}

		} else {
			mids = new HashSet<String>(modelClassToMids.get(modelClass));
		}

		return Collections.unmodifiableSet(mids);

	}

	Set<String> findMetadataForClassOrKind(String clientMid, String classOrKind) {
		Set<String> classes = kindToClasses.get(classOrKind);
		if (classes.isEmpty()) {
			// it's a metadata class
			return findMetadataForClass(clientMid, classOrKind);
		} else {
			// it's a metadata kind. look for classes
			Set<String> result = new HashSet<String>();
			for (String clazz : classes) {
				result.addAll(findMetadataForClass(clientMid, clazz));
			}
			return result;
		}

	}

	private Set<String> findMetadataForClass(String clientMid, String clazz) {
		// If Roo has just started, not all files have been
		// processed yet. Search actively for files with appropriate trigger
		// annotation and get the metadata.
		scanMetadataForClassOnce(clientMid, clazz);

		Set<String> mids = metaDataClassToMids.get(clazz);
		return mids;
	}

	private void scanMetadataForClassOnce(String clientMid, String clazz) {
		if (!scannedMetadataClasses.contains(clazz)) {
			// metadataService.
			Set<Class<?>> triggers = metadataClassToTriggers.get(clazz);
			for (Class<?> trigger : triggers) {
				Set<ClassOrInterfaceTypeDetails> cids = typeLocationService
						.findClassesOrInterfaceDetailsWithAnnotation(new JavaType(trigger));
				for (ClassOrInterfaceTypeDetails cid : cids) {
					String mid = MidUtils.get(clazz).createIdentifier(cid);
					if (!clientMid.equals(mid)) { // What about indirect
													// recursion? Can we rely on
													// MetadataService to detect
													// it?
						metadataService.get(mid);
					}
				}
			}

			scannedMetadataClasses.add(clazz);
		}

	}

	@Override
	public Set<String> getDependentMetadata(MetadataItem upstreamMetadataItem) {

		JavaType modelClass = null;
		if (upstreamMetadataItem instanceof ItdMetadata) {
			// some WicketMetadata has changed
			if (upstreamMetadataItem instanceof ModelClassAware) {
				// some model class aware metadata has changed.
				ModelClassAware mca = (ModelClassAware) upstreamMetadataItem;
				modelClass = mca.getModelClass();
			}
			Class<?> upstreamWicketMetadataClass = upstreamMetadataItem.getClass();

			return getDependentMetadata(upstreamWicketMetadataClass, modelClass);
		} else {
			// some other metadata has changed. Let's assume it is a modelClass
			// an let's see, whether there is Wicket Metadata that depends on
			// it.
			ItdTypeDetailsProvidingMetadataItem itdMetadata = (ItdTypeDetailsProvidingMetadataItem) upstreamMetadataItem;
			ItdTypeDetails itdTypeDetails = itdMetadata.getMemberHoldingTypeDetails();
			if (itdTypeDetails == null) {
				return Collections.emptySet();
			}
			modelClass = itdTypeDetails.getName();
			return Collections.unmodifiableSet(new HashSet<String>(modelClassToMids.get(modelClass)));
		}

	}

	@Override
	public Set<String> getDependentMetadata(Class<?> wicketMetadataClass, JavaType modelClass) {
		if (wicketMetadataClass == null) {
			throw new IllegalArgumentException("wicketMetadataClass must be set.");
		}

		Set<String> mids = new HashSet<String>();
		// get all metadata, that has searched exactly for the given
		// modelClass and metadataClass
		if (modelClass != null) {
			mids.addAll(modelClassAndClassOrKindToClientMids.get(key(modelClass, wicketMetadataClass.getName())));
		}
		// get all metadata, that has searched for the metadata class without
		// restricting to a modelClass
		mids.addAll(modelClassAndClassOrKindToClientMids.get(key(null, wicketMetadataClass.getName())));

		// get all metadata, that has searched for the given modelClass and
		// for the kind that the metadataClass belongs to.
		String kind = classToKind.get(wicketMetadataClass.getName());
		if (kind != null) {
			if (modelClass != null) {
				mids.addAll(modelClassAndClassOrKindToClientMids.get(key(modelClass, kind)));
			}
			// metadata that has searched for the metadata kind without
			// restricting to a modelClass
			mids.addAll(modelClassAndClassOrKindToClientMids.get(key(null, kind)));
		}
		return Collections.unmodifiableSet(mids);

	}

	@Override
	public void registerMetadata(MetadataItem metadataItem) {
		JavaType modelClass = null;
		if (metadataItem instanceof ModelClassAware) {
			modelClass = ((ModelClassAware) metadataItem).getModelClass();
		}
		registerMetadata(metadataItem.getId(), modelClass);
	}

	public void registerMetadata(String mid, JavaType modelClass) {
		// if it was registered before, remove that, since it could be assigned
		// to another modelClass
		deregisterMetadata(mid);
		metaDataClassToMids.put(MetadataIdentificationUtils.getMetadataClass(mid), mid);
		if (modelClass != null) {
			midToModelClass.put(mid, modelClass);
			modelClassToMids.put(modelClass, mid);
		}
	}

	@Override
	public void deregisterMetadata(String mid) {
		metaDataClassToMids.remove(MetadataIdentificationUtils.getMetadataClass(mid), mid);
		JavaType javaType = midToModelClass.remove(mid);
		if (javaType != null) {
			modelClassToMids.remove(javaType, mid);
		}

	}

	@Override
	public void registerMetadataClass(Class<?> kind, Class<?> metadataClass, Set<Class<?>> triggers) {
		deregisterMetadataClass(metadataClass);
		if (kind != null) {
			kindToClasses.put(kind.getName(), metadataClass.getName());
			classToKind.put(metadataClass.getName(), kind.getName());
		}
		metadataClassToTriggers.putAll(metadataClass.getName(), triggers);

	}

	@Override
	public void deregisterMetadataClass(Class<?> metadataClass) {
		String kind = classToKind.remove(metadataClass.getName());
		if (kind != null) {
			kindToClasses.remove(kind, metadataClass.getName());
		}
		metadataClassToTriggers.removeAll(metadataClass.getName());
	}

	private static WicketMetadataKey key(JavaType modelClass, String classOrKind) {
		return new WicketMetadataKey(modelClass, classOrKind);
	}

	private static class WicketMetadataKey {
		JavaType modelClass;
		String classOrKind;

		public WicketMetadataKey(JavaType modelClass, String classOrKind) {
			super();
			this.modelClass = modelClass;
			this.classOrKind = classOrKind;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((classOrKind == null) ? 0 : classOrKind.hashCode());
			result = prime * result + ((modelClass == null) ? 0 : modelClass.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			WicketMetadataKey other = (WicketMetadataKey) obj;
			if (classOrKind == null) {
				if (other.classOrKind != null)
					return false;
			} else if (!classOrKind.equals(other.classOrKind))
				return false;
			if (modelClass == null) {
				if (other.modelClass != null)
					return false;
			} else if (!modelClass.equals(other.modelClass))
				return false;
			return true;
		}

	}
}
